本文讨论的内容适用于 C++ 的绝大多数实现。(但是如果您使用的实现真的怪到家了,那谁也帮不了你)
(如果哪里出现了事实性错误请指出)
前置知识:浮点数
C++ 的基础类型里有三种浮点数类型,分别是 float,double,long double。浮点数使用 IEEE 754 标准,由于不是本文的重点内容,不做过多介绍。对于本文,你只需要知道:浮点数能表示的全都是形如 的数,其中 都是整数,且范围有限制。浮点数不是实数,不能表示所有的数。
参见(维基百科):IEEE 754
前置知识:舍入模式
舍入模式是浮点环境的一部分,它影响着 C++ 程序中如何进行舍入。
通常情况下舍入模式有四种,分别对应头文件 <cfenv> 内定义的四个宏:
FE_DOWNWARD,向负无穷大舍入FE_TONEAREST,向最接近可表示值舍入FE_TOWARDZERO,向零舍入FE_UPWARD,向正无穷大舍入
可以用 fegetround 函数获取舍入模式,fesetround 设置舍入模式,同样位于 <cfenv> 中。默认的舍入模式是 FE_NEAREST,也就是向最接近可表示值舍入。如果遇到中值,通常向偶数舍入。
从实数到浮点数的转换
浮点算术运算符和数学函数得到的结果是实数,可能不能精确地表示成浮点数,此时需要舍入。
运行时的算术运算符和数学函数,按照当前舍入模式舍入到一个可以表示的值。
算术运算符、平方根 sqrt、浮点求余 fmod、融合乘加 fma 及它们的类似运算,通常是保证准确的。也就是说,它们的行为相当于做无限精度的实数运算,最后再按照舍入模式转换到浮点数。
除此之外的各种数学函数牺牲了精度而加速了运行效率,因此精度误差可能会更大,不过仍然受舍入模式影响。不过影响偏向于瞎影响,甚至会发生向下舍比向上入还大的诡异情况
就算是这些保证准确的运算,也可能会受到缩略等优化的影响而得到预期以外的结果。
编译期的算术运算符和数学函数,和上面类似,但是它们不受浮点运行环境影响,永远表现为默认舍入模式。
从字符串到浮点数的转换
浮点数能表示的全都是形如 的数,所以就连 都表示不了。
如果你在程序里写下了类似 0.1 这种字面量,就要被转换成浮点数。编译期的字面量转换和舍入模式无关,一律表现为默认舍入模式。
scanf,cin,atof,strtof 等需要将字符串转化为浮点数的时候,按照当前舍入模式舍入到一个可以表示的值。
从浮点数到字符串的转换
printf,cout,to_string 等需要将浮点数转化为字符串的时候,一律向最接近舍入,如果遇到中值,向偶数舍入。
从浮点数到整数的转换
floor,ceil,trun 系列函数做它们字面意思上的事情。
round 系列函数不受舍入模式影响。当遇到两个整数的中值时,它向远离零的方向取整。
nearbyint 和 rint 系列函数受到舍入模式影响。如果舍入模式是 FE_TONEAREST,当遇到两个整数的中值时,它们向偶数取整。对于其他几个模式,行为上分别等同于 floor,ceil,trun。
输出为同级别浮点数的取整函数绝不会上溢或下溢。不过,输出为整数的取整函数(例如 ll 开头的),或者把输出浮点数转化为整数,可能会超出整数类型的范围。
浮点数向整数的类型转换永远向零舍入,类似 trun。
从浮点数到浮点数的转换
如果把精度更高的浮点数转换为精度更低的浮点数,例如 double 转换为 float,可能出现低精度浮点数无法表示的情况,此时按照当前舍入模式进行舍入。
从整数到浮点数的转换
类似地,如果把精度更高的整数转换为精度更低的浮点数,例如 long long 转换为 double,也会出现类似的问题,此时按照当前舍入模式进行舍入。
舍入到偶数是个啥?
关于舍入到偶数的情况:例如舍入之前的值完全精确地表示为二进制小数 XXX.XXXY1,但是由于种种原因最后的 1 必须被舍入掉。假如 Y 是 1,那么就会入,进位到 XXX.ZZZ0(ZZZ0 是 XXX1 加一进位的结果)。如果 Y 是 0,那么最后的 1 舍去,变成 XXX.XXX0。舍入后新的最后一位永远是 0。
十进制数也是一样,舍入之前的值完全精确地表示为十进制小数 XXX.XXXY5,最后的 5 必须要舍入掉。如果这个 Y 是奇数就入,否则就舍。舍入后的最后一位也永远是偶数。
注意是完全精确,如果你尝试 printf("%.1lf", 1.05);,那么会输出 1.1,不满足舍入到偶数,因为 1.05 要转换成浮点数会首先被舍入为十进制数 1.0500000000000000444089209850062616169452667236328125,并不是完全精确地表示为十进制数 1.05。